using System;
using System.Collections;
using System.Collections.Generic;
using Fadd.Parser.Evaluator.Tokens;
#if TEST
using Xunit;
#endif

namespace Fadd.Parser.Evaluator
{
    /// <summary>
    /// Parses a condition string and compares it to an item
    /// </summary>
    /// <remarks>
    /// This class is NOT threadsafe.
    /// </remarks>
    public class Evaluator
    {
        private object _instance;
        private int _conditionIndex;  // first parameter contains the conditions
        private readonly string _conditions;
        private readonly object[] _values;
        private ParseTree _parseTree;
        private readonly int _indexStartPos;

        /// <summary>
        /// Initializes a new instance of the <see cref="Evaluator"/> class.
        /// </summary>
        /// <param name="conditionString">Conditions, should look like: "Age >= ? AND FirstName == ?".</param>
        /// <param name="parameters">Parameters used in the condition, for instance 17 and Jonas in the example above.</param>
        /// <example>
        /// Evaluator = new Evaluator("FirstName = ? AND Age = ?", "Adam", 10});
        /// </example>
        public Evaluator(string conditionString, params object[] parameters)
        {
            Check.NotEmpty(conditionString, "conditionString");
            Check.Require(parameters, "parameters");
            _conditions = conditionString;
            _values = parameters;
            _indexStartPos = -1;

        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Evaluator"/> class.
        /// </summary>
        /// <param name="conditionAndParameters">First index is conditions, the rest is parameters.</param>
        /// <example>
        /// Evaluator = new Evaluator(new object[]{"FirstName = ?", "Adam"});
        /// </example>
        public Evaluator(object[] conditionAndParameters)
        {
            Check.Require(conditionAndParameters, "conditionAndParameters");

            _conditions = (string)conditionAndParameters[0];
            _values = conditionAndParameters;
            _indexStartPos = 0;
        }


        /// <summary>
        /// Evaluates an instance.
        /// </summary>
        /// <param name="instance">The instance.</param>
        /// <returns></returns>
        public bool Evaluate(object instance)
        {
            Check.Require(instance, "instance");

            _instance = instance;
            if (_parseTree == null)
            {
                Parser parser = new Parser();
                _parseTree = parser.Parse(_conditions);
            }

            _conditionIndex = _indexStartPos;
            return Evaluate(_parseTree.Root);
        }

        /// <summary>
        /// Go through a list and find all items.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="items"></param>
        /// <returns></returns>
        public IList<T> Evaluate<T>(IList<T> items)
        {
            if (_parseTree == null)
            {
                Parser parser = new Parser();
                _parseTree = parser.Parse(_conditions);
            }

            List<T> foundItems = new List<T>();
            foreach (T t in items)
            {
                _instance = t;
                _conditionIndex = _indexStartPos;
                if (Evaluate(_parseTree.Root))
                    foundItems.Add(t);
            }
            return foundItems;
        }

        /// <summary>
        /// Evaluate all tokens in the same level.
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        private bool Evaluate(Token token)
        {
            List<int> values = new List<int>();
            for (int i = 0; i < token.Count; ++i)
            {
                Token treeToken = token[i];
                if (treeToken is Context)
                    values.Add(Evaluate(treeToken) ? 1 : 0);
                else if (treeToken is And)
                    values.Add(2);
                else if (treeToken is Or)
                    values.Add(3);
                else
                    values.Add(CheckItem(token, ref i) ? 1 : 0);
            }

            if (values.Count == 0)
                throw new InvalidOperationException("Failed to find any conditions to evaluate");

            if (values.Count == 1)
                return values[0] == 1;

            // repeat until there are just two values.
            bool result = values[0] == 1;
            int index = 0;
            while (index < values.Count - 1)
            {
                int cond = values[++index];
                bool value2 = values[++index] == 1;
                switch (cond)
                {
                    case 2: // AND
                        result = result && value2;
                        break;
                    case 3: // OR
                        result = result || value2;
                        break;
                }
            }

            return result;
        }

        //Used to simplify testing
        private static bool Evaluate(object instance, object[] conditions)
        {
            Evaluator evaluater = new Evaluator(conditions);
            return evaluater.Evaluate(instance);
        }
#if TEST
        [Fact]
        private static void TestSimpleConditions()
        {
            TestUser user = new TestUser();
            user.FirstName = "Jonas";
            user.Age = 10;

            Assert.False(Evaluate(user, new object[] { "FirstName != ?", "Jonas" }));
            Assert.True(Evaluate(user, new object[] { "FirstName == ?", "Jonas" }));
            Assert.True(Evaluate(user, new object[] { "Age > ?", 5 }));
            Assert.True(Evaluate(user, new object[] { "Age == ?", 10 }));
            Assert.True(Evaluate(user, new object[] { "Age <= ?", 10 }));
            Assert.True(Evaluate(user, new object[] { "Age >= ?", 10 }));
            Assert.False(Evaluate(user, new object[] { "Age < ?", 10 }));
            Assert.False(Evaluate(user, new object[] { "Age > ?", 10 }));
            Assert.False(Evaluate(user, new object[] { "Age != ?", 10 }));
            Assert.False(Evaluate(user, new object[] { "Age <> ?", 10 }));
        }

        [Fact]
        private static void TestDoubleConditions()
        {
            TestUser user = new TestUser();
            user.FirstName = "Jonas";
            user.Age = 10;

            Assert.False(Evaluate(user, new object[] { "FirstName != ? AND Age = ?", "Jonas", 10 }));
            Assert.False(Evaluate(user, new object[] { "FirstName == ? AND Age != ?", "Jonas", 10 }));
            Assert.False(Evaluate(user, new object[] { "FirstName == ? AND Age == ?", "Jonas", 8 }));
            Assert.False(Evaluate(user, new object[] { "FirstName == ? AND Age == ?", "J", 8 }));
            Assert.True(Evaluate(user, new object[] { "FirstName == ? AND Age = ?", "Jonas", 10 }));
            Assert.True(Evaluate(user, new object[] { "FirstName == ? OR Age == ?", "Jonas", 10 }));
            Assert.True(Evaluate(user, new object[] { "FirstName == ? OR Age != ?", "Jonas", 10 }));
            Assert.True(Evaluate(user, new object[] { "FirstName != ? OR Age == ?", "Jonas", 10 }));
            Assert.True(Evaluate(user, new object[] { "FirstName == ? OR Age == ?", "Jonas", 8 }));
            Assert.True(Evaluate(user, new object[] { "FirstName == ? OR Age == ?", "J", 10 }));
        }

        [Fact]
        private static void TestThreeConditions()
        {
            TestUser user = new TestUser();
            user.FirstName = "Jonas";
            user.Age = 10;

            Assert.False(Evaluate(user, new object[] { "FirstName != ? AND Age = ? OR Age = ?", "Jonas", 5, 5}));
            Assert.True(Evaluate(user, new object[] { "FirstName == ? AND Age = ? OR Age = ?", "Jonas", 4, 10 }));
        }

        [Fact]
        private static void TestNestedConditions()
        {
            TestUser user = new TestUser();
            user.FirstName = "Jonas";
            user.Age = 10;

            Assert.True(Evaluate(user, new object[] { "FirstName == ? OR (FirstName = ? AND Age = ?)", "Adm", "Jonas", 10 }));
            Assert.True(Evaluate(user, new object[] { "FirstName == ? OR (FirstName = ? AND Age = ?)", "Jonas", "Adam", 10 }));
        }

        [Fact]
        private static void TestIN()
        {
            TestUser user = new TestUser();
            user.FirstName = "Jonas";
            user.Age = 10;
            user.MyType = MyType.B;

            Assert.True(Evaluate(user, new object[] { "MyType IN (?)", "C,D,B" }));
            Assert.True(Evaluate(user, new object[] { "MyType IN (?)", "0,1,2,3" }));
            Assert.True(Evaluate(user, new object[] { "Age IN (?)", "1,10,12" }));
            Assert.True(Evaluate(user, new object[] {"Age IN (?)", new int[] {4, 5, 6, 7, 8, 9, 10, 12, 12}}));
            Assert.False(Evaluate(user, new object[] { "Age IN (?)", "1,12" }));
            Assert.False(Evaluate(user, new object[] { "Age IN (?)", "1,12," }));
            Assert.Throws(typeof (FormatException), delegate
                                                        {
                                                            Evaluate(user,
                                                                     new object[] {"Age IN (?)", new string[] {"abc"}});
                                                        });
        }

        [Fact]
        private static void TestFind()
        {
            List<TestUser> users = new List<TestUser>();
            for (int i = 0; i < 10; ++i)
            {
                TestUser user = new TestUser();
                user.FirstName = "First" + i;
                user.LastName = "Last" + i;
                user.Age = 10 + i;
                user.Status = i%2;
                users.Add(user);
            }
                
            Evaluator evaluator = new Evaluator("(FirstName = ? AND LastName = ?) OR LastName = ?", "First1", "Last1", "Last2");
            List<TestUser> result = users.FindAll(delegate(TestUser user) { return evaluator.Evaluate(user); });
            Assert.Equal(2, result.Count);
        }
#endif
        /// <summary>
        /// Token should be a property name.
        /// </summary>
        /// <param name="token"></param>
        /// <param name="tokenIndex"></param>
        /// <returns></returns>
        /// <remarks>
        /// Will take three of the tokens (property CONDITION target) and evaluate them.
        /// </remarks>
        /// <exception cref="FormatException">If values can't be casted to property type</exception>
        private bool CheckItem(Token token, ref int tokenIndex)
        {
            ++_conditionIndex; // index in the _conditions string

            string propertyName = PropertyName(token[tokenIndex]);
            object propertyValue = Property.Get(_instance, propertyName);
            Token condition = token[++tokenIndex];
            ++tokenIndex; // ignore '?'
            if (_conditionIndex >= _conditions.Length)
                throw new InvalidOperationException("Missing condition at position: " + _conditionIndex + " (parentToken: " + token + ", subIndex: " + tokenIndex + ").");

            object conditionValue = _values[_conditionIndex];

            if (condition is Equal)
            {
                if ((propertyValue is Enum && conditionValue is int) || (propertyValue is int && conditionValue is Enum))
                        return (int)conditionValue == (int) propertyValue;
                if ((propertyValue is Enum && conditionValue is string) || (propertyValue is int && conditionValue is string))
                    return conditionValue.ToString() == propertyValue.ToString();

                return conditionValue.Equals(propertyValue);
            }

            if (condition is NotEqual)
            {
                if ((propertyValue is Enum && conditionValue is int) || (propertyValue is int && conditionValue is Enum))
                    return (int) conditionValue != (int) propertyValue;
                if ((propertyValue is Enum && conditionValue is string) ||
                    (propertyValue is int && conditionValue is string))
                    return conditionValue.ToString() != propertyValue.ToString();

                return !conditionValue.Equals(propertyValue);
            }
            if (condition is In)
            {
                if (conditionValue is string)
                {
                    string svalue = (string) conditionValue;
                    string[] inValues = svalue.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries);
                    for (int i = 0; i < inValues.Length; ++i)
                    {
                        inValues[i] = inValues[i].Trim();
                        if (propertyValue is Enum)
                        {
                            // first char is digit, interpret it as int ;)
                            if (char.IsDigit(inValues[i][0]) && Convert.ToInt32(inValues[i]) == (int)propertyValue)
                                return true;
                            // compare strings
                            if (propertyValue.ToString().Equals(inValues[i]))
                                return true;
                            // compare values.
                            if (propertyValue.Equals(inValues[i]))
                                return true;
                        }
                        if (propertyValue is int)
                        {
                            if (propertyValue.Equals(Convert.ToInt32(inValues[i])))
                                return true;
                        }
                        else if (propertyValue.Equals(inValues[i]))
                            return true;
                    }
                    return false;
                }
                if (conditionValue is IEnumerable)
                {
                    Type propertyType = propertyValue.GetType();
                    foreach (object o in (IEnumerable)conditionValue)
                        if (Convert.ChangeType(o, propertyType).Equals(propertyValue))
                            return true;
                    return false;
                }

                throw new NotSupportedException("We got an IN clause but could not handle it's values.");
            }

            if (!(propertyValue is IComparable))
                throw new NotSupportedException(propertyName + " is not IComparable and can therefore not use anything else than 'equal' and 'not equal'.");
            if (!(propertyValue is IComparable))
                throw new NotSupportedException("Condition value for " + propertyName + " is not IComparable and can therefore not use anything else than 'equal' and 'not equal'.");

            IComparable propCompare = (IComparable) propertyValue;
            IComparable condCompare = (IComparable) conditionValue;

            if (condition is LessOrEqual)
                return propCompare.CompareTo(condCompare) <= 0;
            if (condition is LessThan)
                return propCompare.CompareTo(condCompare) < 0;
            if (condition is MoreOrEqual)
                return propCompare.CompareTo(condCompare) >= 0;
            if (condition is MoreThan)
                return propCompare.CompareTo(condCompare) > 0;

            throw new InvalidOperationException("Expected a proper condition, got: " + token);
        }

        private static string PropertyName(Token token)
        {
            if (!(token is Tokens.Property))
                throw new InvalidOperationException("Expected a property name, got: " + token);
            return ((Tokens.Property)token).Name;
        }
    }

#if DEBUG
    /// <summary>
    /// 
    /// </summary>
    public enum MyType
    {
        /// <summary>
        /// 
        /// </summary>
        A,
        /// <summary>
        /// 
        /// </summary>
        B
    }
    /// <summary>
    /// User used during testing.
    /// </summary>
    public class TestUser
    {
        private string _firstName;
        private string _lastName;
        private int _age;
        private int _status;
        private MyType _myType;

        /// <summary>
        /// Gets or sets the first name.
        /// </summary>
        /// <value>The first name.</value>
        public string FirstName
        {
            get { return _firstName; }
            set { _firstName = value; }
        }

        /// <summary>
        /// Gets or sets the last name.
        /// </summary>
        /// <value>The last name.</value>
        public string LastName
        {
            get { return _lastName; }
            set { _lastName = value; }
        }

        /// <summary>
        /// Gets or sets the age.
        /// </summary>
        /// <value>The age.</value>
        public int Age
        {
            get { return _age; }
            set { _age = value; }
        }

        /// <summary>
        /// Gets or sets the status.
        /// </summary>
        /// <value>The status.</value>
        public int Status
        {
            get { return _status; }
            set { _status = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        public MyType MyType
        {
            get { return _myType; }
            set { _myType = value; }
        }
    }
#endif
}
